/*******************************************************************************
 * CLI - A simple command line interface.
 * Copyright (C) 2016-2021 Daniele Pallastrelli
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ******************************************************************************/

#include <boost/test/unit_test.hpp>
#include "cli/cli.h"
#include "cli/clifilesession.h"

using namespace std;
using namespace cli;
using namespace cli::detail;

namespace {

string ExtractFirstPrompt(const stringstream& o)
{
    auto content = o.str();
    std::size_t pos = content.find_first_of('>');
    return content.substr(0, pos);
}

string ExtractLastPrompt(const stringstream& o)
{
    auto content = o.str();
    std::size_t pos = content.find_last_of('\n');
    content = content.substr(pos+1);
    pos = content.find_last_of('>');
    return content.substr(0, pos);
}

/* takes

cli> sub> foo
bar
sub> 

and gives

foo
bar

*/
string ExtractContent(const stringstream& o)
{
    auto content = o.str();
    // last line
    auto lastNL = content.find_last_of('\n');
    auto lastLine = content.substr(lastNL+1);
    content = content.substr(0, lastNL);
    auto pos = content.find(lastLine);
    return content.substr(pos+lastLine.size());
}

void UserInput(Cli& cli, stringstream& oss, const string& input)
{
    oss.str("");
    oss.clear();

    stringstream iss;
    iss.str(input + '\n');
    CliFileSession session(cli, iss, oss);
    session.Start();
}

} // namespace

BOOST_AUTO_TEST_SUITE(CliSuite)

BOOST_AUTO_TEST_CASE(Basics)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} );
    rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} );

    Cli cli(std::move(rootMenu));

    stringstream oss;

    UserInput(cli, oss, "help");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK(ExtractContent(oss).find("int_cmd") != string::npos);
    BOOST_CHECK(ExtractContent(oss).find("<int_par>") != string::npos);
    BOOST_CHECK(ExtractContent(oss).find("string_cmd") != string::npos);
    BOOST_CHECK(ExtractContent(oss).find("<string_par>") != string::npos);

    UserInput(cli, oss, "int_cmd 42");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");

    UserInput(cli, oss, "int_cmd wrong_int_parameter");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "int_cmd 42 0");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "string_cmd foo");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "foo");

    UserInput(cli, oss, R"(string_cmd "foo 'bar' \"foo\\2")");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), R"(foo 'bar' "foo\2)");
}

BOOST_AUTO_TEST_CASE(parameters)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("char_cmd", [](ostream& out, char par){ out << par << "\n"; }, "char_cmd help", {"char_par"} );
    rootMenu->Insert("unsigned_char_cmd", [](ostream& out, unsigned char par){ out << static_cast<unsigned int>(par) << "\n"; }, "unsigned_char_cmd help", {"unsigned_char_par"} );
    rootMenu->Insert("signed_char_cmd", [](ostream& out, signed char par){ out << static_cast<int>(par) << "\n"; }, "signed_char_cmd help", {"signed_char_par"} );
    rootMenu->Insert("short_cmd", [](ostream& out, short par){ out << par << "\n"; }, "short_cmd help", {"short_par"} );
    rootMenu->Insert("unsigned_short_cmd", [](ostream& out, unsigned short par){ out << par << "\n"; }, "unsigned_short_cmd help", {"unsigned_short_par"} );
    rootMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} );
    rootMenu->Insert("unsigned_int_cmd", [](ostream& out, unsigned int par){ out << par << "\n"; }, "unsigned_int_cmd help", {"unsigned_int_par"} );
    rootMenu->Insert("long_cmd", [](ostream& out, long par){ out << par << "\n"; }, "long_cmd help", {"long_par"} );
    rootMenu->Insert("unsigned_long_cmd", [](ostream& out, unsigned long par){ out << par << "\n"; }, "unsigned_long_cmd help", {"unsigned_long_par"} );
    rootMenu->Insert("long_long_cmd", [](ostream& out, long long par){ out << par << "\n"; }, "long_long_cmd help", {"long_long_par"} );
    rootMenu->Insert("unsigned_long_long_cmd", [](ostream& out, unsigned long long par){ out << par << "\n"; }, "unsigned_long_long_cmd help", {"unsigned_long_long_par"} );
    rootMenu->Insert("float_cmd", [](ostream& out, float par){ out << par << "\n"; }, "float_cmd help", {"float_par"} );
    rootMenu->Insert("double_cmd", [](ostream& out, double par){ out << par << "\n"; }, "double_cmd help", {"double_par"} );
    rootMenu->Insert("long_double_cmd", [](ostream& out, long double par){ out << par << "\n"; }, "long_double_cmd help", {"long_double_par"} );
    rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} );

    Cli cli(std::move(rootMenu));

    stringstream oss;

    UserInput(cli, oss, "char_cmd a");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "a");
    UserInput(cli, oss, "char_cmd ' '");
    BOOST_CHECK_EQUAL(ExtractContent(oss), " ");
    UserInput(cli, oss, "char_cmd aa");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "unsigned_char_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "unsigned_char_cmd -42");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_char_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_char_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "signed_char_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "signed_char_cmd -42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "-42");
    UserInput(cli, oss, "signed_char_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "signed_char_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "short_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "short_cmd -42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "-42");
    UserInput(cli, oss, "short_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "short_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "unsigned_short_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "unsigned_short_cmd -42");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_short_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_short_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "int_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "int_cmd -42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "-42");
    UserInput(cli, oss, "int_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "int_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "unsigned_int_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "unsigned_int_cmd -42");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_int_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_int_cmd 99999999999999999999999999999999999999999");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "long_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "long_cmd -42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "-42");
    UserInput(cli, oss, "long_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "unsigned_long_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "unsigned_long_cmd -42");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_long_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "long_long_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "long_long_cmd -42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "-42");
    UserInput(cli, oss, "long_long_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "unsigned_long_long_cmd 42");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");
    UserInput(cli, oss, "unsigned_long_long_cmd -42");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
    UserInput(cli, oss, "unsigned_long_long_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "float_cmd 0.1");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "0.1");
    UserInput(cli, oss, "float_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "double_cmd 0.1");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "0.1");
    UserInput(cli, oss, "double_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);

    UserInput(cli, oss, "long_double_cmd 0.1");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "0.1");
    UserInput(cli, oss, "long_double_cmd a");
    BOOST_CHECK(ExtractContent(oss).find("wrong command:") != string::npos);
}

BOOST_AUTO_TEST_CASE(freeform)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("cmd_printer_by_ref", [](ostream& out, const std::vector<std::string>& args){
        for (const auto& entry : args) {
            out << entry << "*";
        }
        out << "\n";
    }, "cmd_printer help", {"<string values>"} );

    rootMenu->Insert("cmd_printer_by_value", [](ostream& out, std::vector<std::string> args){
        for (const auto& entry : args) {
            out << entry << "*";
        }
        out << "\n";
    }, "cmd_printer help", {"<string values>"} );

    Cli cli(std::move(rootMenu));
    stringstream oss;

    UserInput(cli, oss, R"(cmd_printer_by_value a b 'c d e' f)");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "a*b*c d e*f*");

    UserInput(cli, oss, R"(cmd_printer_by_ref a b 'c d e' f)");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "a*b*c d e*f*");

    // empty parameters
    UserInput(cli, oss, R"(cmd_printer_by_value)");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "");
}

BOOST_AUTO_TEST_CASE(borderLine)
{
    auto rootMenu = make_unique<Menu>("cli");
    Cli cli(std::move(rootMenu));

    stringstream oss;

    UserInput(cli, oss, "");
    BOOST_CHECK_EQUAL(oss.str(), "cli> cli> ");

    UserInput(cli, oss, "\t");
    BOOST_CHECK_EQUAL(oss.str(), "cli> cli> ");
}

BOOST_AUTO_TEST_CASE(Submenus)
{
    auto rootMenu = make_unique<Menu>("cli");
    auto subMenu = make_unique<Menu>("sub");
    subMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} );
    subMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} );
    auto subSubMenu = make_unique<Menu>("subsub");
    subSubMenu->Insert("double_int_cmd", [](ostream& out, int par1, int par2){ out << par1 << par2 << "\n"; } );
    subSubMenu->Insert("double_string_cmd", [](ostream& out, const string& par1, const string& par2){ out << par1 << par2 << "\n"; } );
    subMenu->Insert(std::move(subSubMenu));
    rootMenu->Insert(std::move(subMenu));

    Cli cli(std::move(rootMenu));

    stringstream oss;

    UserInput(cli, oss, "help");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK(ExtractContent(oss).find("sub") != string::npos);
    BOOST_CHECK(ExtractContent(oss).find("help") != string::npos);
    BOOST_CHECK(ExtractContent(oss).find("exit") != string::npos);

    // from the root menu

    UserInput(cli, oss, "sub int_cmd 42");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");

    UserInput(cli, oss, "sub string_cmd foo");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "foo");

    UserInput(cli, oss, "sub subsub double_int_cmd 42 0");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "420");

    UserInput(cli, oss, "sub subsub double_string_cmd foo bar");
    BOOST_CHECK_EQUAL(ExtractFirstPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "cli");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "foobar");

    // enter the sub menu

    UserInput(cli, oss, "sub\nint_cmd 42");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "sub");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "42");

    UserInput(cli, oss, "sub\nstring_cmd foo");
    BOOST_CHECK_EQUAL(ExtractLastPrompt(oss), "sub");
    BOOST_CHECK_EQUAL(ExtractContent(oss), "foo");
}

BOOST_AUTO_TEST_CASE(EnterActions)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("int_cmd",
                     [](ostream &out, int par) { out << par << "\n"; },
                     "int_cmd help", {"int_par"});
    rootMenu->Insert(
        "string_cmd",
        [](ostream &out, const string &par) { out << par << "\n"; },
        "string_cmd help", {"string_par"});

    Cli cli(std::move(rootMenu));
    bool enterActionDone = false;

    cli.EnterAction(
        [&enterActionDone](std::ostream &) noexcept { enterActionDone = true; });

    stringstream oss;

    UserInput(cli, oss, "exit");
    BOOST_CHECK(enterActionDone);
}

BOOST_AUTO_TEST_CASE(ExitActions)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} );
    rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} );

    Cli cli(std::move(rootMenu));
    bool exitActionDone = false;
    cli.ExitAction([&](std::ostream&) noexcept { exitActionDone=true; });

    stringstream oss;

    UserInput(cli, oss, "exit");
    BOOST_CHECK(exitActionDone);
}

BOOST_AUTO_TEST_CASE(Exceptions)
{
    auto rootMenu = make_unique<Menu>("cli");
    rootMenu->Insert("stdexception", [](ostream&){ throw std::logic_error("myerror"); } );
    rootMenu->Insert("customexception", [](ostream&){ throw 42; } );

    Cli cli(std::move(rootMenu));

    stringstream oss;

    // std exception type, no custom handler
    BOOST_CHECK_NO_THROW( UserInput(cli, oss, "stdexception") );
    BOOST_CHECK_EQUAL(ExtractContent(oss), "myerror");

    // std exception type, custom handler
    bool excActionDone = false;
    cli.StdExceptionHandler( [&](std::ostream&, const std::string&, const std::exception&) noexcept { excActionDone = true; } );
    BOOST_CHECK_NO_THROW( UserInput(cli, oss, "stdexception") );
    BOOST_CHECK(excActionDone);

    // custom exception
    BOOST_CHECK_NO_THROW( UserInput(cli, oss, "customexception") );
}

BOOST_AUTO_TEST_SUITE_END()
